package board;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JFrame;

import board.grid.Grid;
import board.grid.RangeGrid;
import board.grid.Space;
import board.pieces.BasicPiece;
import board.pieces.Piece;
import board.renders.*;
import board.renders.adapters.CellRenderAdapter;
import board.renders.adapters.OccupantRenderAdapter;
import board.renders.basics.BasicBoardBGRender;
import board.renders.basics.BasicCellRender;
import board.renders.basics.BasicLineRender;
import board.renders.basics.BasicOccupantRender;

/** A simple display for the <code>Grid</code>
 *  in the Swing environment.  
 *  
 *  Modifying the contents displayed by this
 *  class are manipulated by manipulating the
 *  contents of the <code>Grid<T></code> or
 *  by adding/changing the renders used.  
 *  
 *  *This Class does NOT extend JFame - it is
 *  not a window.  To use this, you will need
 *  to add it to some High-level element such
 *  as a JApplet, JDialog, or JFrame.  
 *  
 *  *This Class is NOT thread save - please
 *  only manipulate it in the AWT-EVENT-THREAD
 *  after it has been displayed.  
 * 
 * @author R.Wisniewski1101
 */
public class GridPane extends JComponent {
	private static final long serialVersionUID = 5672289885297310743L;
	/** The grid being displayed by this component.  
	 *  This grid must be set in the constructor
	 *  so that the preferred size can be calculated.  
	 *  This allows for the container that holds
	 *  this component to use <code>pack()</code>
	 *  to put his component at the default
	 *  resolution of 64 px per space.  
	 */
	private Grid<?> grid;
	/** The renders for the occupant classes.  
	 *  Each render is tied to an occupant class
	 *  (which can include interfaces) such
	 *  renders can be added to full utilize
	 *  the properties of that class, and such
	 *  that special render rules can be applied
	 *  for new classes specifically.  
	 */
	private Map<Class<?>, OccupantRender<?>> occupantRenders;
	/** The render used for any occupant who
	 *  has no applicable renders in the map.  
	 */
	private BasicOccupantRender bor;
	/** The render set for the background.  
	 *  By default, this is the Basic
	 *  version which is just a white background
	 *  area.  
	 */
	private BoardBGRender background;
	/** This is the render used to draw the
	 *  grid lines.  By default they are GRAY.  
	 */
	private LineRender lines;
	/** This is the render used to render the
	 *  background for the cells.  This may seem
	 *  redundant because there is a general
	 *  background image, but this image can
	 *  be different for each cell, and can be
	 *  null (none).  
	 */
	private CellRender cells;
	/** The current dimension of the cells.  
	 *  This is recalculated each time this
	 *  component is redrawn.  
	 */
	private int currentCellSize;
	/** The amount of space to add on the
	 *  left and right of the grid.  
	 */
	private int padding_x;
	/** The amount of space to add on the
	 *  top and bottom of the grid.  
	 */
	private int padding_y;
	/** Create a GridPane to display
	 *  the specified <code>Grid<?></code>.   
	 * 
	 * @param grid - the <code>Grid<?></code> to display.  
	 */
	public GridPane (Grid<?> grid) {
		//store the grid
		this.grid = grid;
		//create the renders map
		occupantRenders = new HashMap<Class<?>, OccupantRender<?>>();
		//create the default renders
		bor = new BasicOccupantRender();
		lines = new BasicLineRender();
		cells = new BasicCellRender();
		background = new BasicBoardBGRender();
		//prime the default renders;
		bor.prime(this);
		lines.prime(this);
		cells.prime(this);
		background.prime(this);
		//set the default cell size
		currentCellSize = 64;
	}
	/** Get the current width, in pixels, used by the grid.  
	 * 
	 * @return the width of the grid.  
	 */
	public int getGridWidth() {return grid.getWidth() * (currentCellSize + 1) -  1;}
	/** Get the current height, in pixels, used by the grid.  
	 * 
	 * @return the height of the grid.  
	 */
	public int getGridHeight() {return grid.getHeight() * (currentCellSize + 1) - 1;}
	/** Add a render for a specific class.  
	 * 
	 * @param type - the class to which to apply the render
	 * @param render - the render to apply
	 */
	public void setOccupantRender(Class<?> type, OccupantRender<?> render) {occupantRenders.put(type, render);}
	/** Change the render that creates the
	 *  background image for the board.  
	 * 
	 * @param render - the new render for the background
	 */
	public void setBoardRender(BoardBGRender render) {background = render;}
	/** Change the render that creates the
	 *  line images for the board.  
	 *   
	 * @param render - the new render for the lines
	 */
	public void setLineRender(LineRender render) {lines = render;}
	/** Change the render that creates the
	 *  background for the cells on the board. 
	 * 
	 * @param render - the new cell render.  
	 */
	public void setCellRender(CellRender render) {cells = render;}
	@Override //JComponent
	public void paintComponent(Graphics g) {
		//if there is no grid to draw, stop.  
		if(grid == null)
			return;
		//calculate the cell size based on the width
		int w = (getWidth() + 1) / grid.getWidth() - 1;
		//if size is negative, do invert it.  
		if(w < 0)
			w = -w;
		//calculate the cell size based on the height
		int h = (getHeight() + 1) / grid.getHeight() - 1;
		//if size is negative, do invert it. 
		if(h < 0)
			h = -h;
		//use the smaller size to ensure that the whole grid is visible
		currentCellSize = w < h ? w : h;
		//if the size is 0, stop because there is no room to draw.  
		if(currentCellSize <= 0)
			return;
		//calculate padding
		padding_x = (getWidth() - grid.getWidth() * currentCellSize) / 2;
		padding_y = (getHeight() - grid.getHeight() * currentCellSize) / 2;
		//ensure that the parent class is prepared to draw.  
		this.revalidate();
		/* Up-cast because we need more methods than Graphics has.  
		   This should always work because Graphics2D is used by
		   swing but Graphics is the parameter for AWT compatibility.  
		 */
		Graphics2D g2 = (Graphics2D) g;
		super.paintComponent(g2);
		//draw background
		g2.drawImage(background.getBG(getGridWidth(), getGridHeight()), padding_x, padding_y, this);
		//draw cells
		displayCells(g2);
		//draw grid lines
		if(lines.isVerticalFirst()) {
			displayVerticalLines(g2);
			displayHorizontalLines(g2);
		} else {
			displayHorizontalLines(g2);
			displayVerticalLines(g2);
		}
		//draw occupants
		displayOccupants(g2);
	}
	/** Convert a point coordinate (in pixels)
	 *  into a space coordinate for this grid.  
	 * 
	 * @param p - the point to convert
	 * @return - the space that contains that point (null is it is in the padding)
	 */
	public Space pointToSpace(Point p) {
		if(p.x >= padding_x && p.x <= padding_x + getGridWidth() && p.y >= padding_y && p.y <= padding_y + getGridHeight()) {
			Space s = Space.get(
				((int)p.getX() - padding_x) / (currentCellSize + 1) + grid.getMinCol(),
				grid.getHeight() - ((int)p.getY() - padding_y) / (currentCellSize + 1) - 1 + grid.getMinRow()
			);
			return grid.isValidSpace(s) ? s : null; }
		else return null;
	}
	/** Convert a space coordinate from the
	 *  grid into the point (in pixels) where
	 *  the space should be rendered.  This
	 *  point represents the top left corner
	 *  of the space.  
	 * 
	 * @param s - the space to convert
	 * @return - the point corresponding to the top left corner of the space (null if space is not valid)
	 */
	public Point spaceToPoint(Space s) {
		return grid.isValidSpace(s) ? new Point(
			padding_x + (s.getColumn() - grid.getMinCol()) * (currentCellSize + 1),
			padding_y + (grid.getHeight() - s.getRow() - 1 - grid.getMinRow()) * (currentCellSize + 1)
		) : null;
	}
	@Override //JComponent
	public Dimension getPreferredSize() {
		return new Dimension(
				getGridWidth(),
				getGridHeight()
			);
	}
	/** Draw the cells background images as
	 *  created by the cells render.  
	 * 
	 * @param g - the area on which to draw
	 */
	private void displayCells(Graphics2D g) {
		for(Space s : grid.allValidSpaces()) {
			Image i = cells.getCellBG(s, currentCellSize, currentCellSize);
			Point pos = spaceToPoint(s);
			g.drawImage(i, pos.x, pos.y, this);
		}
	}
	/** Draw the horizontal grid lines as created
	 *  by the lines render.  
	 * 
	 * @param g - the area on which to draw
	 */
	private void displayHorizontalLines(Graphics2D g) {
		for(int y = 1; y < grid.getHeight(); ++y) {
			Image i = lines.getHorizontalLine(y, getGridWidth());
			int pos = (y * (currentCellSize + 1) - 1) - i.getHeight(this)/2;
			g.drawImage(i, padding_x, pos + padding_y, this);
		}
	}
	/** Draw the vertical grid lines as created
	 *  by the lines render.  
	 * 
	 * @param g - the area on which to draw
	 */
	private void displayVerticalLines(Graphics2D g) {
		for(int x = 1; x < grid.getWidth(); ++x) {
			Image i = lines.getVerticalLine(x, getGridHeight());
			int pos = (x * (currentCellSize + 1) - 1) - i.getWidth(this)/2;
			g.drawImage(i, pos + padding_x, padding_y, this);
		}
	}
	/** Draw all of the occupants of the grid.  
	 * 
	 * @param g - the area on which to draw
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void displayOccupants(Graphics2D g) {
		Collection<?> occupants = grid.getAll();
		for(Object current : occupants) {
			OccupantRender render = findRender(current);
			Point p = spaceToPoint(grid.getCurrentSpace(current));
			g.drawImage(render.getImage(current, currentCellSize, currentCellSize), p.x, p.y, this);
		}
	}
	/** Finds the render set the the given object.  
	 *  If there is no render set for that object
	 *  type, the bor is returned.  
	 * 
	 * @param occupant - the object to get a render for
	 * @return - the appropriate render
	 */
	private OccupantRender<?> findRender(Object occupant) {
		Class<?> current = occupant.getClass();
		while(true) {
			if(occupantRenders.containsKey(current))
				return occupantRenders.get(current);
			Collection<Class<?>> interfaces = Arrays.asList(current.getInterfaces());
			Class<?> sup = current.getSuperclass();
			if(sup == null)
				return bor;
			interfaces.removeAll(Arrays.asList(sup.getInterfaces()));
			for(Class<?> i : interfaces)
				if(occupantRenders.containsKey(i))
					return occupantRenders.get(i);
			current = sup;
		}
	}
	/** Test main methods used to test the main facilities of this board.  
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		JFrame frame = new JFrame("TEST");
		Grid<Object> pieces = new RangeGrid<Object>(4, 4);
		Piece p = null;
		for(int x = 0; x < 4; ++x)
			for(int y = 0; y < 4; ++y) {
				p = new BasicPiece();
				p.putSetfOnGrid(pieces, Space.get(x, y));
				p.setColor( x % 2 != y % 2 ? Color.red : Color.WHITE);
			}
		GridPane pane = new GridPane(pieces);
		pane.setOccupantRender(Piece.class, new OccupantRenderAdapter<Piece>() {
			public Image getImage(Piece occupant, int width, int height) {	
				Image image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
				Graphics draw = image.getGraphics();
				draw.setColor(occupant.getColor());
				draw.fillOval(width / 6, height / 6, 2 * width / 3, 2 * height / 3);
				return image;
			}
		});
		pane.setCellRender(new CellRenderAdapter() {
			public Image getCellBG(Space space, int width, int height) {
				Image image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
				Graphics draw = image.getGraphics();
				boolean col = space.getColumn() % 2 == 0;
				boolean row = space.getRow() % 2 == 0;
				if(col != row) 
					draw.setColor(new Color(0x40C040));
				else
					draw.setColor(new Color(0x404040));
				draw.fillRect(0, 0, width, height);
				return image;
			}
		});
		frame.add(pane);
		frame.pack();
		frame.setVisible(true);
	}
}
